﻿using System;
using System.Collections.Generic;
using System.Linq;
using Core.Domain;
using Core.Domain.Lexems;
using Core.SyntaxAnalysis.Classes;

namespace Core.SyntaxAnalysis
{
    public class SyntaxAnalyzer
    {
        private Lexem m_current;
        private int m_currentLexemIndex = 0;
        private List<CompilerError> m_errors;
        private List<Lexem> m_lexems;
        private Lexem m_previous = null;

        public SyntaxAnalyzer()
        {
            Env.Clear();
        }

        public Program Parse(List<Lexem> lexems, ref List<CompilerError> errors)
        {
            m_lexems = lexems;
            m_errors = errors;
            if (m_lexems.Count == 0)
            {
                return null;
            }
            m_current = m_lexems[m_currentLexemIndex];
            return ReadProgram();
        }

        private bool Match(LexemType lt, string value = null)
        {
            while (m_current.Type == LexemType.NewLine)
            {
                m_currentLexemIndex++;
                m_current = m_lexems[m_currentLexemIndex];
            }
            if (lt.HasFlag(m_current.Type) && (value == null || m_current.StrValue.ToUpper() == value.ToUpper()))
            {
                m_currentLexemIndex++;
                if (m_currentLexemIndex < m_lexems.Count)
                {
                    m_previous = m_current;
                    m_current = m_lexems[m_currentLexemIndex];
                }
                return true;
            }
            return false;
        }

        public Program ReadProgram()
        {
            try
            {
                if (!Match(LexemType.Terminal, TerminalWords.Program))
                {
                    throw new SyntaxException(new CompilerError(m_errors.Count + 1, Resources.NoProgramErrorMessage, m_previous));
                }
                var name = m_current;
                if (!Match(LexemType.ID))
                {
                    throw new SyntaxException(new CompilerError(m_errors.Count + 1, Resources.NoProgramNameErrorMessage, m_previous));
                }
                if (!Match(LexemType.Terminal, ";"))
                {
                    throw new SyntaxException(new CompilerError(m_errors.Count + 1, Resources.NoSemicolonAfterProgramNameErrorMessage, m_previous));
                }
                var subPrograms = ReadSubPrograms().ToList();
                var declarations = DeclrsRead();
                if (declarations == null) // declarations equal to null only if there was a mistake, otherwise it will refer either to declarations or to an EmptyDeclrs
                {
                    throw new SyntaxException(new CompilerError(m_errors.Count + 1, Resources.MisusedVarErrorMessage, m_previous));
                }
                subPrograms.AddRange(ReadSubPrograms());
                if (m_current.StrValue.ToUpper() != TerminalWords.Begin)
                {
                    throw new SyntaxException(new CompilerError(m_errors.Count + 1, Resources.NoProgramBeginErrorMessage, m_previous));
                }
                var body = ReadStatementsBlock();
                if (!Match(LexemType.Terminal, "."))
                {
                    throw new SyntaxException(new CompilerError(m_errors.Count + 1, Resources.NoEndDotErrorMessage, m_previous));
                }
                return new Program(name, subPrograms, declarations, body);
            }
            catch (SyntaxException ex)
            {
                m_errors.Add(ex.Error);
                return null;
            }
        }

        public Declrs DeclrsRead()
        {
            if (!Match(LexemType.Terminal, TerminalWords.Var))
            {
                return Declrs.EmptyDeclrs(); // there might be no declarations, returning dummy
            }
            return DeclrBodyRead();
        }

        public Declrs DeclrBodyRead()
        {
            var variables = ReadVariables();
            Declrs result = null;
            while (variables != null)
            {
                if (!Match(LexemType.Terminal, ":"))
                {
                    // если блок переменных всё-таки есть, то обязательно должны быть структуры вида <имена переменных> <:> <тип данных>
                    throw new SyntaxException(new CompilerError(m_errors.Count + 1, Resources.NoColonInVariablesListErrorMessage, m_previous));
                }
                var type = DataType();
                if (type == null)
                {
                    throw new SyntaxException(new CompilerError(m_errors.Count + 1, Resources.NoDataTypeInVariablesListErrorMessage, m_previous));
                }
                if (!Match(LexemType.Terminal, ";"))
                {
                    throw new SyntaxException(new CompilerError(m_errors.Count + 1, Resources.NoSemicolonAfterVariablesListErrorMessage, m_previous));
                }
                if (result == null)
                {
                    result = new Declrs(variables, type);
                }
                else
                {
                    result.Append(new Declaration(variables, type));
                }
                variables = ReadVariables();
            }
            return result; // было слово VAR, но не было списка переменных - ошибка
        }

        public List<Identificator> ReadVariables()
        {
            if (!Match(LexemType.ID))
            {
                return null;
            }
            var variables = new List<Identificator> {Identificator.Create(m_previous)};
            while (Match(LexemType.Terminal, ","))
            {
                if (!Match(LexemType.ID))
                {
                    throw new SyntaxException(new CompilerError(m_errors.Count + 1, Resources.ExtraCommaInVariablesListErrorMessage, m_previous));
                }
                variables.Add(Identificator.Create(m_previous));
            }
            return variables;
        }

        public DataType DataType()
        {
            if (Match(LexemType.Terminal, TerminalWords.Real) || Match(LexemType.Terminal, TerminalWords.Integer))
            {
                return VariableType.PickType(m_previous);
            }
            return null;
        }

        public Block ReadStatementsBlock()
        {
            if (!Match(LexemType.Terminal, TerminalWords.Begin))
            {
                return null;
            }
            var statements = StmtsRead();
            var result = new Block(statements);
            if (!Match(LexemType.Terminal, TerminalWords.End))
            {
                throw new SyntaxException(new CompilerError(m_errors.Count + 1, Resources.NoEndInBlockErrorMessage, m_previous));
            }
            return result;
        }

        public Stmts StmtsRead()
        {
            Stmts result = null;
            var statement = ReadStatement();
            var noSemicolon = false;
            Lexem expectedSemicolon = null;
            while (statement != null)
            {
                if (noSemicolon)
                {
                    throw new SyntaxException(new CompilerError(m_errors.Count + 1, Resources.NoSemicolonErrorMessage, expectedSemicolon));
                }
                if (result == null)
                {
                    result = new Stmts(statement);
                }
                else
                {
                    result.Append(statement);
                }
                expectedSemicolon = m_previous;
                noSemicolon = ReadSemicolons();
                statement = ReadStatement();
            }
            return result;
        }

        public bool ReadSemicolons()
        {
            var noSemicolon = true;
            while (Match(LexemType.Terminal, ";"))
            {
                noSemicolon = false;
            }
            return noSemicolon;
        }

        public Stmt ReadStatement()
        {
            switch (m_current.Type)
            {
                case LexemType.ConstInt:
                    return ReadLabel();
                case LexemType.Terminal:
                    switch (m_current.StrValue)
                    {
                        case TerminalWords.Begin:
                            return ReadStatementsBlock();
                        case TerminalWords.If:
                            return ReadIf();
                        case TerminalWords.For:
                            return ReadFor();
                        case TerminalWords.Goto:
                            return ReadGoto();
                        case TerminalWords.Repeat:
                            return ReadRepeat();
                        case TerminalWords.While:
                            return ReadWhile();
                        case TerminalWords.Case:
                            return ReadCase();
                    }
                    break;
                case LexemType.ID:
                    return ReadAssign();
            }
            return null;
        }

        public Stmt ReadLabel()
        {
            if (!Match(LexemType.ConstInt))
            {
                throw new SyntaxException(new CompilerError(m_errors.Count + 1, Resources.LabelNameErrorMessage, m_previous));
            }
            var number = m_previous;
            if (!Match(LexemType.Terminal, ":"))
            {
                throw new SyntaxException(new CompilerError(m_errors.Count + 1, Resources.LabelFormatErrorMessage, m_previous));
            }
            return Label.Create(number, ReadStatement());
        }

        public Stmt ReadAssign()
        {
            Stmt stmt = null;
            if (Match(LexemType.ID))
            {
                var funcName = m_previous;
                if (Match(LexemType.Terminal, ":="))
                {
                    var sign = m_previous;
                    stmt = new Assign(funcName, ReadExpression(), sign);
                }
                else if (Match(LexemType.Terminal, "("))
                {
                    stmt = new FuncCall(funcName, ReadFuncCall());
                }
                else
                {
                    throw new SyntaxException(new CompilerError(m_errors.Count, "После идентификатора должен идти знак присваивания или перечисление аргументов в скобках", m_previous));
                }
            }
            return stmt;
        }

        public Expr ReadExpression()
        {
            var x = ReadJoin();
            while (Match(LexemType.Terminal, TerminalWords.Or))
            {
                x = new BoolBinary(m_previous, x, ReadJoin());
            }
            return x;
        }

        public Expr ReadJoin()
        {
            var x = ReadEquality();
            while (Match(LexemType.Terminal, TerminalWords.And))
            {
                x = new BoolBinary(m_previous, x, ReadJoin());
            }
            return x;
        }

        public Expr ReadEquality()
        {
            var x = ReadRelation();
            while (Match(LexemType.Terminal, "=") || Match(LexemType.Terminal, "<>"))
            {
                x = new Relation(m_previous, x, ReadRelation());
            }
            return x;
        }

        public Expr ReadRelation()
        {
            var x = ReadMathExpression();
            while (Match(LexemType.Terminal, "<") || Match(LexemType.Terminal, "<=") || Match(LexemType.Terminal, ">") || Match(LexemType.Terminal, ">="))
            {
                x = new Relation(m_previous, x, ReadMathExpression());
            }
            return x;
        }

        public Expr ReadMathExpression()
        {
            var x = ReadTerm();
            while (Match(LexemType.Terminal, "+") || Match(LexemType.Terminal, "-"))
            {
                x = new Arith(m_previous, x, ReadTerm());
            }
            return x;
        }

        public Expr ReadTerm()
        {
            var x = ReadUnary();
            while (Match(LexemType.Terminal, "*") || Match(LexemType.Terminal, "/"))
            {
                x = new Arith(m_previous, x, ReadUnary());
            }
            return x;
        }

        public Expr ReadUnary()
        {
            if (Match(LexemType.Terminal, "-") || Match(LexemType.Terminal, TerminalWords.Not))
            {
                return new Unary(m_previous, ReadUnary());
            }
            return ReadFactor();
        }

        public Expr ReadFactor()
        {
            Expr x = null;
            if (Match(LexemType.Terminal, "("))
            {
                x = ReadExpression();
                if (!Match(LexemType.Terminal, ")"))
                {
                    throw new Exception(Resources.ClosingBracketErrorMessage);
                }
            }
            else if (Match(LexemType.ConstInt))
            {
                x = new IntegerConst(m_previous);
            }
            else if (Match(LexemType.ConstDouble))
            {
                x = new FloatConst(m_previous);
            }
            else if (Match(LexemType.ID))
            {
                var funcName = m_previous;
                if (Match(LexemType.Terminal, "("))
                {
                    x = new FuncCall(funcName, ReadFuncCall());
                }
                else
                {
                    x = Identificator.Find(m_previous);
                    if (x == null)
                    {
                        throw new SyntaxException(new CompilerError(m_errors.Count, "Переменная не объявлена", m_previous));
                    }
                }
            }
            else if (Match(LexemType.Terminal, "TRUE") || Match(LexemType.Terminal, "FALSE"))
            {
                x = BooleanConst.GetBooleanConst(m_previous);
            }
            return x;
        }

        public List<Expr> ReadFuncCall()
        {
            var parameters = ReadFuncParameters();
            if (!Match(LexemType.Terminal, ")"))
            {
                throw new SyntaxException(new CompilerError(m_errors.Count + 1, Resources.ClosingBracketAfterFunctionCallErrorMessage, m_previous));
            }
            return parameters;
        }

        public List<Expr> ReadFuncParameters()
        {
            var x = ReadExpression();
            var parameters = new List<Expr>();
            while (x != null)
            {
                parameters.Add(x);
                if (!Match(LexemType.Terminal, ","))
                {
                    break;
                }
                x = ReadExpression();
            }
            return parameters;
        }

        public IF ReadIf()
        {
            if (!Match(LexemType.Terminal, TerminalWords.If))
            {
                throw new SyntaxException(new CompilerError(m_errors.Count + 1, Resources.GeneralIFErrorMessage, m_previous));
            }
            var condition = ReadExpression();
            if (!Match(LexemType.Terminal, TerminalWords.Then))
            {
                throw new SyntaxException(new CompilerError(m_errors.Count + 1, Resources.NoThenInIfErrorMessage, m_previous));
            }
            var then = ReadStatement();
            ReadSemicolons();
            if (then == null)
            {
                throw new SyntaxException(new CompilerError(m_errors.Count + 1, Resources.NoStatementAfterThenErrorMessage, m_previous));
            }
            if (!Match(LexemType.Terminal, TerminalWords.Else))
            {
                return new IF(condition, then, null);
            }
            var elseStmt = ReadStatement();
            if (elseStmt == null)
            {
                throw new SyntaxException(new CompilerError(m_errors.Count + 1, Resources.MisusedElseErrorMessage, m_previous));
            }
            return new IF(condition, then, elseStmt);
        }

        public FOR ReadFor()
        {
            if (!Match(LexemType.Terminal, TerminalWords.For))
            {
                throw new SyntaxException(new CompilerError(m_errors.Count + 1, Resources.NoForErrorMessage, m_previous));
            }
            var init = ReadAssign();
            if (init == null)
            {
                throw new SyntaxException(new CompilerError(m_errors.Count + 1, Resources.NoVariableInForErrorMessage, m_previous));
            }
            if (!Match(LexemType.Terminal, TerminalWords.To) && !Match(LexemType.Terminal, TerminalWords.Downto))
            {
                throw new SyntaxException(new CompilerError(m_errors.Count + 1, Resources.NoDirectionInForErrorMessage, m_previous));
            }
            var endvalue = ReadExpression();
            if (endvalue == null)
            {
                throw new SyntaxException(new CompilerError(m_errors.Count + 1, Resources.NoForEndErrorMessage, m_previous));
            }
            if (!Match(LexemType.Terminal, TerminalWords.Do))
            {
                throw new SyntaxException(new CompilerError(m_errors.Count + 1, Resources.NoDoErrorMessage, m_previous));
            }
            var body = ReadStatement();
            if (body == null)
            {
                throw new SyntaxException(new CompilerError(m_errors.Count + 1, Resources.NoBodyErrorMessage, m_previous));
            }
            return new FOR(init, m_previous, endvalue, body);
        }

        public GOTO ReadGoto()
        {
            if (!Match(LexemType.Terminal, TerminalWords.Goto))
            {
                throw new SyntaxException(new CompilerError(m_errors.Count + 1, Resources.NoGotoErrorMessage, m_previous));
            }
            if (!Match(LexemType.ConstInt))
            {
                throw new SyntaxException(new CompilerError(m_errors.Count + 1, Resources.WrongLabelTypeErrorMessage, m_previous));
            }
            return new GOTO(m_previous);
        }

        public REPEAT ReadRepeat()
        {
            if (!Match(LexemType.Terminal, TerminalWords.Repeat))
            {
                throw new SyntaxException(new CompilerError(m_errors.Count + 1, "Ошибка в заголовке цикла REPEAT", m_previous));
            }

            Block body = null;
            var statements = StmtsRead();
            if (statements != null)
            {
                body = new Block(statements);
            }

            if (!Match(LexemType.Terminal, TerminalWords.Until))
            {
                throw new SyntaxException(new CompilerError(m_errors.Count + 1, "Нет ключевого слова UNTIL", m_previous));
            }
            var condition = ReadExpression();
            if (condition == null)
            {
                throw new SyntaxException(new CompilerError(m_errors.Count + 1, "Нет условия окончания цикла REPEAT", m_previous));
            }
            return new REPEAT(condition, body);
        }

        public WHILE ReadWhile()
        {
            if (!Match(LexemType.Terminal, TerminalWords.While))
            {
                throw new SyntaxException(new CompilerError(m_errors.Count + 1, "Ошибка в заголовке цикла WHILE", m_previous));
            }
            var condition = ReadExpression();

            if (condition == null)
            {
                throw new SyntaxException(new CompilerError(m_errors.Count + 1, "Нет условия окончания цикла WHILE", m_previous));
            }
            if (!Match(LexemType.Terminal, TerminalWords.Do))
            {
                throw new SyntaxException(new CompilerError(m_errors.Count + 1, "Нет ключевого слова do", m_previous));
            }
            var body = ReadStatement();
            return new WHILE(condition, body);
        }

        public Case ReadCase()
        {
            if (!Match(LexemType.Terminal, TerminalWords.Case))
            {
                throw new SyntaxException(new CompilerError(m_errors.Count + 1, "Case must be here", m_previous));
            }

            if (!Match(LexemType.ID))
            {
                throw new SyntaxException(new CompilerError(m_errors.Count + 1, "Case works only on variables", m_previous));
            }


            if (!Match(LexemType.Terminal, TerminalWords.Of))
            {
                throw new SyntaxException(new CompilerError(m_errors.Count + 1, "No of in case", m_previous));
            }

            var l = ReadLabel();
            var s = ReadSemicolons();

            if (!Match(LexemType.Terminal, TerminalWords.End))
            {
                throw new SyntaxException(new CompilerError(m_errors.Count + 1, "Case must end with end keyword", m_previous));
            }



            // TODO: implement case
            return new Case();
        }

        public IEnumerable<SubProgram> ReadSubPrograms()
        {
            if (Match(LexemType.Terminal, TerminalWords.Procedure))
            {
                yield return ReadProcedure();
                foreach (var sp in ReadSubPrograms())
                {
                    yield return sp;
                }
            }
            else if (Match(LexemType.Terminal, TerminalWords.Function))
            {
                yield return ReadFunction();
                foreach (var sp in ReadSubPrograms())
                {
                    yield return sp;
                }
            }
        }

        public SubProgram ReadProcedure()
        {
            if (!Match(LexemType.ID))
            {
                throw new SyntaxException(new CompilerError(m_errors.Count + 1, "После ключевого слова procedure должно идти имя процедуры", m_previous));
            }
            var name = new Identificator(m_previous);
            var args = Declrs.EmptyDeclrs();
            if (Match(LexemType.Terminal, "("))
            {
                args = DeclrBodyRead();
                if (!Match(LexemType.Terminal, ")"))
                {
                    throw new SyntaxException(new CompilerError(m_errors.Count + 1, "Нет закрывающей круглой скобки", m_previous));
                }
            }

            if (!Match(LexemType.Terminal, ";"))
            {
                throw new SyntaxException(new CompilerError(m_errors.Count + 1, "Прототип процедуры должен заканчиваться точкой с запятой", m_previous));
            }

            var variables = DeclrsRead();

            var body = ReadStatementsBlock();
            if (body == null)
            {
                throw new SyntaxException(new CompilerError(m_errors.Count + 1, "Нет тела процедуры", m_previous));
            }

            if (ReadSemicolons())
            {
                throw new SyntaxException(new CompilerError(m_errors.Count + 1, "После процедуры должна стоять точка с запятой", m_previous));
            }

            return new Procedure(name, args, variables, body);
        }

        public SubProgram ReadFunction()
        {
            return new Function(null, null, null, null, null);
        }
    }
}